Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an HttpClient interface and NodeHttpClient implementation. #1218

Merged
merged 10 commits into from
Aug 13, 2021

Conversation

dcr-stripe
Copy link
Contributor

Notify

r? @richardm-stripe + @tjfontaine-stripe (for extra 👀 )

Summary

Adds an HttpClient interface and moves out the Node http/https request logic into a NodeHttpClient implementation. Update Stripe config to allow specifying a custom HttpClient.

This is the first step in allowing a custom client (say one that uses fetch for #997).

The interface is summarized in the typescript. In brief:

  • makeRequest returns a Response as a promise.
  • Response#toStream(streamCompleteCallback) returns a streamable version of the content. The Node behavior is unchanged here of just returning the raw response itself with the headers tacked on.
  • Response#toJSON() consumes the response and returns the JSON representation as a promise.

This should effectively be a no-op, but this should be released as its own version in case there are issues.

You can see an example rough implementation of this for fetch in #1208.

Test plan

Added unit tests both to StripeResource and to NodeHttpClient itself.

Copy link

@jt-stripe jt-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few questions about the TS definitions and a suggestion for how to expose this... I didn't carefully review all the implementation details yet!

* Converts the response content into a JSON object, failing if JSON
* couldn't be parsed.
*/
toJSON(): Promise<object>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a union type that's, like, all possible stripe resource types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so unfortunately. @richardm-stripe to confirm?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't but could easily have one if we wanted.

Though object feels better to me here, since HTTPClient should be mostly stripe-domain-agnostic.

types/net/net.d.ts Outdated Show resolved Hide resolved
Comment on lines +8 to +9
const defaultHttpAgent = new http.Agent({keepAlive: true});
const defaultHttpsAgent = new https.Agent({keepAlive: true});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to move this into the constructor? It'd be nice to not have side effects on import in the case that this is being imported in a non-node environment.

Copy link
Contributor

@richardm-stripe richardm-stripe Aug 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do need a singleton -- if the user instantiates multiple stripe clients (without overriding the agent) then they should all share the same connection pool. We could lazily instantiate it when the constructor is called, though. Although, maybe we should instead ensure that this file is not required in non-node environments? It is NodeHttpClient after all.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is resolved by @dcr-stripe 's latest changes

lib/stripe.js Outdated Show resolved Hide resolved
lib/stripe.js Outdated Show resolved Hide resolved
}

/** Helper to make a consistent timeout error across implementations. */
static makeTimeoutError() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great

* The streamCompleteCallback should be invoked by the response
* implementation when the stream has been consumed.
*/
toStream(streamCompleteCallback: () => void): any; //eslint-disable-line @typescript-eslint/no-explicit-any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we pursue generic type parameters?

export interface HttpClient<Response> {
  makeRequest(...): Promise<Response>
}

interface HttpClientResponse<RawResponse, Stream> {
  ...
  getRawResponse(): RawResponse,
  toStream(...): Stream,
  ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (err) {
return done(err);
}
const result = await stripe.charges.create(options.data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is missing a .catch so if stripe.charges.create fails the error won't be properly propagated to the test runner

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -208,6 +216,98 @@ describe('StripeResource', () => {
);
});

it('throws an error on invalid JSON', (done) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be anyway to add a test for when there's a connection error after the headers have been transmitted, but halfway through when the body is being transmitted?

(this should be pretty similar to the test case above on line #200).

That's the one case I can think of where (correct me if I'm wrong) I don't think we have any test coverage but it still would be good to know that this PR is consistent with the previous behavior (and that fetch and nodehttpclient will behave consistently, too)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@richardm-stripe richardm-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is absolutely wonderful. In particular I think the decision to use a promise interface for HttpClient vs. more conservatively attempting to preserve the callbacky interface represents a huge improvement to the understandability of the code and will make a far better experience for implementers of this interface.

I left a few comments, though just some ideas, nothing blocking. I'll re-assign you to the PR, feel free to assign me again once you've had a look/responded and I will approve.

Copy link
Contributor Author

@dcr-stripe dcr-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great ideas all around. PTAL @richardm-stripe

@@ -208,6 +216,98 @@ describe('StripeResource', () => {
);
});

it('throws an error on invalid JSON', (done) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* The streamCompleteCallback should be invoked by the response
* implementation when the stream has been consumed.
*/
toStream(streamCompleteCallback: () => void): any; //eslint-disable-line @typescript-eslint/no-explicit-any
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if (err) {
return done(err);
}
const result = await stripe.charges.create(options.data);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor

@richardm-stripe richardm-stripe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, (pending fixing that TS complaint about any)

dcr-stripe and others added 3 commits August 13, 2021 14:04
Co-authored-by: Richard Marmorstein <52928443+richardm-stripe@users.noreply.github.com>
@dcr-stripe dcr-stripe merged commit 6f9c519 into master Aug 13, 2021
@dcr-stripe dcr-stripe deleted the dcr-http-client-interface branch August 13, 2021 18:11
bpinto added a commit to bpinto/stripe-node that referenced this pull request Jan 21, 2022
Custom request timeout message was never used since we were incorrectly
requiring the HttpClient class.

I believe this has stopped working on stripe#1218.
bpinto added a commit to bpinto/stripe-node that referenced this pull request Jan 21, 2022
Custom request timeout message was never used since we were incorrectly
requiring the HttpClient class.

I believe this has stopped working on stripe#1218.
bpinto added a commit to bpinto/stripe-node that referenced this pull request Jan 28, 2022
Custom request timeout message was never used since we were incorrectly
requiring the HttpClient class.

I believe this has stopped working on stripe#1218.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants